﻿using System;
using T0yK4T.Tools;
using System.Collections.Generic;
using ToyChat.Server;
using MongoDB;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

namespace ToyChat.BuiltinModules
{
    /// <summary>
    /// This class is used to get permissions from the database server-side
    /// </summary>
    public class ServerPermissionsHandler : ComponentBase, IPermissionsHandler
    {
        private IServer server;
        private MongoCollection<UserPermissions> permissionsCollection;
        private MongoCollection<DefaultChannelPermissions> defaultCollection;
        private Dictionary<int, DefaultChannelPermissions> cachedDefaults = new Dictionary<int, DefaultChannelPermissions>();

        /// <summary>
        /// Initializes a new instance of <see cref="ServerPermissionsHandler"/> using the specified <see cref="IServer"/>
        /// </summary>
        /// <param name="server">The server that "owns" this handler</param>
        public ServerPermissionsHandler(IServer server)
        {
            this.server = server;
            this.permissionsCollection = this.server.GlobalDBHandler.GetCollection<UserPermissions>();
            this.defaultCollection = this.server.GlobalDBHandler.GetCollection<DefaultChannelPermissions>();
            this.defaultCollection.CreateIndex(new IndexKeysDocument("ChannelID", 1), new IndexOptionsDocument(false));
            base.LogInformation("Initialized");
            this.FixDeadReferences();
        }

        private void FixDeadReferences()
        {
            foreach (DefaultChannelPermissions def in this.defaultCollection.FindAll())
            {
                IChannel channel = this.server.GlobalDBHandler.FindChannel(def.ChannelID);
                if (channel == null)
                {
                    base.LogWarning("DefaultChannelPemissions: Removing dead reference to Channel with ID: {0}", def.ChannelID);
                    this.defaultCollection.Remove(Query.EQ("ChannelID", def.ChannelID));
                    //this.defaultCollection.Remove(def);
                }
            }
            foreach (UserPermissions permissions in this.permissionsCollection.FindAll())//.Documents)
            {
                IChannel channel = this.server.GlobalDBHandler.FindChannel(permissions.ChannelID);
                IUserInfo info = this.server.GlobalDBHandler.FindUser(permissions.Username);
                if (channel == null)
                {
                    base.LogWarning("UserPermissions: Removing dead reference to Channel with ID: {0}", permissions.ChannelID);
                    this.permissionsCollection.Remove(Query.EQ("ChannelID", permissions.ChannelID));
                    //this.permissionsCollection.Remove(permissions);
                }
                else if (info == null)
                {
                    base.LogWarning("UserPermissions: Removing dead reference to User with Username: {0}", permissions.Username);
                    this.permissionsCollection.Remove(Query.EQ("Username", permissions.Username));
                    //this.permissionsCollection.Remove(permissions);
                }
            }
        }

        /// <summary>
        /// Checks the permissions of the user with the specified UserID on the channel with the specfieid ChannelID for the specified Permissions
        /// </summary>
        /// <param name="username">The username of the user to check permissions for</param>
        /// <param name="channelID">The ID of the channel to check permissions for</param>
        /// <param name="permissions">The permissions to check for</param>
        /// <returns>True if the user has the specified permissions, otherwise false</returns>
        public bool CheckPermissions(string username, int channelID, ChannelPermissionFlags permissions)
        {
            UserPermissions perms = this.permissionsCollection.FindOne(Query.And(Query.EQ("Username", username), Query.EQ("ChannelID", channelID)));//(p) => p.Username == username && p.ChannelID == channelID);
            if (perms != null)
                return (perms.Permissions & permissions) == permissions;
            else if (!this.cachedDefaults.ContainsKey(channelID))
            {
                DefaultChannelPermissions defPerms = this.defaultCollection.FindOne(Query.EQ("ChannelID", channelID));//(p) => p.ChannelID == channelID);
                if (defPerms == null)
                {
                    defPerms = new DefaultChannelPermissions { Permissions = ChannelPermissionFlags.Speak, ChannelID = channelID };
                    this.defaultCollection.Insert(defPerms);
                }
                this.cachedDefaults.Add(defPerms.ChannelID, defPerms);
                return (defPerms.Permissions & permissions) == permissions;
            }
            else
                return (this.cachedDefaults[channelID].Permissions & permissions) == permissions;
        }

        /// <summary>
        /// Attempts to get the permissions for the user with the specified UserID on the channel with the specified ChannelID
        /// </summary>
        /// <param name="username">The username of the user to get permissions for</param>
        /// <param name="channelID">The ID of the channel to get permissions for</param>
        /// <returns>An <see cref="UserPermissions"/> object containing the permissions of the user if succesful, otherwise a TimeoutException is thrown</returns>
        public UserPermissions GetPermissions(string username, int channelID)
        {
            UserPermissions perms = this.permissionsCollection.FindOne(Query.And(Query.EQ("Username", username), Query.EQ("ChannelID", channelID)));//(p) => p.Username == username && p.ChannelID == channelID);
            if (perms == null)
            {
                DefaultChannelPermissions def = this.defaultCollection.FindOne(Query.EQ("ChannelID", channelID));//(p) => p.ChannelID == channelID);
                perms = new UserPermissions { Permissions = def.Permissions, ChannelID = channelID, Username = username };
            }
            return perms;
        }


        /// <summary>
        /// Stores the specified <see cref="DefaultChannelPermissions"/> in the database
        /// </summary>
        /// <param name="permissions">The permissions to store</param>
        public void StorePermissions(DefaultChannelPermissions permissions)
        {
            DefaultChannelPermissions perms = this.defaultCollection.FindOne(Query.EQ("ChannelID", permissions.ChannelID));//(p) => p.ChannelID == permissions.ChannelID);
            if (perms != null)
                permissions.ID = perms.ID;
            this.defaultCollection.Save(permissions);
        }

        /// <summary>
        /// Stores the specified <see cref="UserPermissions"/> object in the database
        /// </summary>
        /// <param name="permissions">The permissions to store</param>
        public void StorePermissions(UserPermissions permissions)
        {
            UserPermissions perms = this.permissionsCollection.FindOne(Query.And(Query.EQ("Username", permissions.Username), Query.EQ("ChannelID", permissions.ChannelID)));//(p) => p.Username == permissions.Username && p.ChannelID == permissions.ChannelID);
            if (perms != null)
                permissions.ID = perms.ID;
            this.permissionsCollection.Save(permissions);
        }
    }
}